﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Reflection;
using YasharEl.Infrastructure.Diagnostics.Logging;
using YasharEl.Infrastructure.ServiceBus.MessageMutator;

namespace YasharEl.Infrastructure.ServiceBus.Encryption
{
    /// <summary>
    /// Invokes the encryption service to encrypt/decrypt messages
    /// </summary>
    public class EncryptionMessageMutator : IMessageMutator
    {
        #region Static Fields

        readonly static IDictionary<Type, IEnumerable<PropertyInfo>> cache = new ConcurrentDictionary<Type, IEnumerable<PropertyInfo>>();

        #endregion

        #region ReadOnly Fields

        private readonly IEncryptionService encryptionService;
        private readonly ILogger<IEncryptionService> logger;

        #endregion

        #region Properties

        protected IEncryptionService EncryptionService 
        {
            get
            {
                return encryptionService;
            }
        }

        #endregion

        #region Constructors

        public EncryptionMessageMutator(ILogger<IEncryptionService> logger, IEncryptionService encryptionService)
        {
            Contract.Requires<ArgumentNullException>(logger != null, "logger could not be null.");
            Contract.Requires<ArgumentNullException>(encryptionService != null, "encryptionService could not be null.");

            this.logger = logger;
            this.encryptionService = encryptionService;
        }

        #endregion

        #region IMessageMutator Implementation

        public object MutateOutgoing(object message)
        {
            var encryptedProperties = GetEncryptedProperties(message);

            foreach (var encryptedProperty in encryptedProperties)
            {
                if (EncryptionService == null)
                    throw new InvalidOperationException(String.Format("Cannot encrypt field {0} because no encryption service was configured.", encryptedProperty.Name));

                var valueToEncrypt = encryptedProperty.GetValue(message, null);


                if (valueToEncrypt == null)
                    continue;

                if (valueToEncrypt is WireEncryptedString)
                    EncryptWireEncryptedString((WireEncryptedString)valueToEncrypt);
                else
                {
                    encryptedProperty.SetValue(message, EncryptUserSpecifiedProperty(valueToEncrypt), null);
                }


                logger.Debug(encryptedProperty.Name + " encrypted successfully");

            }
            return message;
        }

        public object MutateIncoming(object message)
        {
            var encryptedProperties = GetEncryptedProperties(message);

            foreach (var encryptedProperty in encryptedProperties)
            {
                if (EncryptionService == null)
                    throw new InvalidOperationException(String.Format("Cannot decrypt field {0} because no encryption service was configured.", encryptedProperty.Name));

                var encryptedValue = encryptedProperty.GetValue(message, null);

                if (encryptedValue == null)
                    continue;

                if (encryptedValue is WireEncryptedString)
                    Decrypt((WireEncryptedString)encryptedValue);
                else
                {
                    encryptedProperty.SetValue(message, DecryptUserSpecifiedProperty(encryptedValue), null);
                }

                logger.Debug(encryptedProperty.Name + " decrypted successfully");
            }
            return message;
        }

        #endregion

        #region Private Methods

        private string DecryptUserSpecifiedProperty(object encryptedValue)
        {
            var stringToDecrypt = encryptedValue as string;

            // TODO : Change to service bus specific exception
            if (stringToDecrypt == null)
                throw new InvalidOperationException("Only string properties is supported for convention based encryption, please check your convention");

            var parts = stringToDecrypt.Split(new[] { '@' }, StringSplitOptions.None);

            return EncryptionService.Decrypt(new EncryptedValue
            {
                EncryptedBase64Value = parts[0],
                Base64Iv = parts[1]
            });
        }

        private void Decrypt(WireEncryptedString encryptedValue)
        {
            encryptedValue.Value = EncryptionService.Decrypt(encryptedValue.EncryptedValue);
        }

        private string EncryptUserSpecifiedProperty(object valueToEncrypt)
        {
            var stringToEncrypt = valueToEncrypt as string;

            // TODO : Change to service bus specific exception
            if (stringToEncrypt == null)
                throw new InvalidOperationException("Only string properties is supported for convention based encryption, please check your convention");

            var encryptedValue = EncryptionService.Encrypt(stringToEncrypt);

            return string.Format("{0}@{1}", encryptedValue.EncryptedBase64Value, encryptedValue.Base64Iv);
        }

        private void EncryptWireEncryptedString(WireEncryptedString wireEncryptedString)
        {
            wireEncryptedString.EncryptedValue = EncryptionService.Encrypt(wireEncryptedString.Value);
            wireEncryptedString.Value = null;

        }

        #endregion

        #region Static Methods

        static IEnumerable<PropertyInfo> GetEncryptedProperties(object message)
        {
            var messageType = message.GetType();

            if (!cache.ContainsKey(messageType))
                cache[messageType] = messageType.GetProperties()
                 .Where(property => property.IsEncryptedProperty())
                 .ToList();

            return cache[messageType];
        }

        #endregion
    }
}
